"""
The MIT License (MIT)
Copyright © 2022 Walkline Wang (https://walkline.wang)
Gitee: https://gitee.com/walkline/micropython-esp32-walkie-talkie
"""
from machine import Pin, I2C, I2S
import framebuf
import socket
from utime import sleep_ms
from drivers.button import Button
from drivers.potentiometer import Potentiometer
from drivers.touchpad import TouchPad
from drivers.ssd1306 import SSD1306_I2C
from libs.fontlib import FontLib
from utils.dispatcher import Dispatcher
from utils.wifihandler import WifiHandler

DEBUG = True

# wave sample const
BITS = 16
FORMAT = I2S.MONO
RATE = 16000
BUFF_SIZE = 1024 * 5

# sound effect
SOUND_EFFECT_SIGNALING1 = 'pcm/tone1_bits16_rate16000_mono.wav'
SOUND_EFFECT_CLICK1 = 'pcm/tone2_bits16_rate16000_mono.wav'
SOUND_EFFECT_CLICK2 = 'pcm/tone3_bits16_rate16000_mono.wav'
SOUND_EFFECT_CLICK3 = 'pcm/tone4_bits16_rate16000_mono.wav'

# WAVE_SAMPLES_MV = memoryview(bytearray(BUFF_SIZE))

# fontlib const
FONTFILE = 'fonts/font_JetBrains-Mono-ExtraBold_s24_w11.bin'
# FontMaker_Cli.exe -f "Ubuntu Mono" -s 20 -b -w 11 --input ascii.txt -sh -o font_20.bin
# FontMaker_Cli.exe -f "JetBrains Mono ExtraBold" -s 24 -w 11 --input ascii.txt -sh -o font_24.bin


def printf(msg):
	if DEBUG:
		print(msg)

class Pins(object):
	class ADC(object):
		BATTERY = 34
		CHANNEL = 32

	class I2S_CODEC(object):
		SD = Pin(27)
		WS = Pin(25)
		SCK = Pin(26)

	class I2S_MIC(object):
		SD = Pin(12)
		WS = Pin(14)
		SCK = Pin(13)

	class KEYS(object):
		CHANNEL_UP = 23
		CHANNEL_DOWN = 22
		CD42 = 4
		PTT = 5
		TOUCH = 15

		BUTTONS = [CHANNEL_UP, CHANNEL_DOWN, PTT]
		TOUCHPADS = [TOUCH]

	class OLED(object):
		SDA = 19
		SCL = 18


class UDPClient(object):
	HOST = ('255.255.255.255', 7890)

	def __init__(self):
		self.__client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
		self.__client.setblocking(False)
		self.wave_sample_mv = memoryview(bytearray(BUFF_SIZE))

	def send(self, length, buffer=None):
		buffer = self.wave_sample_mv[0:length] if buffer is None else buffer
		self.__client.sendto(buffer[0:length], (UDPClient.HOST))
		# sleep_ms(2)


class VerifyBoard(object):
	def __init__(self):
		self.__display:SSD1306_I2C		= None
		self.__fontlib:FontLib			= None
		self.__buttons:Button			= None
		self.__touchpad:TouchPad		= None
		self.__chn_adc:Potentiometer	= None
		self.__mic:I2S					= None
		self.__codec:I2S				= None
		self.__led:Pin					= None
		self.__udp_client				= UDPClient()

		max_value						= 800 # adc 读数最大值
		self.__max_channel				= 5 # 旋转刻度数量
		self.__scale_range				= int(max_value / self.__max_channel) # 每刻度读数范围
		self.__channel					= 0

		WifiHandler.set_sta_mode(timeout_sec=120)

		self.__initialize_hardware()
		self.__show_font('loading', center=True)

		# 回调闭包函数
		self.__task_send_wave_sample_via_udp = lambda: self.__send_wave_sample_via_udp()
		self.__task_buttons_timer_callback   = lambda: self.__buttons.timer_callback()
		self.__task_touchpad_check_status    = lambda: self.__touchpad.check_status()
		self.__task_channel_timer_callback   = lambda: self.__chn_adc.timer_callback()

		self.__dispatcher = Dispatcher()
		self.__dispatcher.add_work(self.__task_buttons_timer_callback, 20)
		self.__dispatcher.add_work(self.__task_touchpad_check_status, 20)
		self.__dispatcher.add_work(self.__task_channel_timer_callback, 20)

		self.__clean_display()
		self.__show_font('CH1')

	def __initialize_hardware(self):
		i2c = I2C(0, scl=Pin(Pins.OLED.SCL), sda=Pin(Pins.OLED.SDA))
		i2c_list = i2c.scan()

		if i2c_list:
			self.__display = SSD1306_I2C(128, 64, i2c, addr=i2c_list[0])
			self.__fontlib = FontLib(FONTFILE)

		self.__buttons = Button(
			pin=Pins.KEYS.BUTTONS,
			hold_cb=self.__button_hold_cb,
			release_cb=self.__button_release_cb,
			click_cb=self.__button_click_cb,
			timer_id=None
		)

		self.__touchpad = TouchPad(
			pins=Pins.KEYS.TOUCHPADS,
			touch_cb=self.__touchpad_cb,
			timer_id=None
		)

		self.__chn_adc = Potentiometer(
			pins=Pins.ADC.CHANNEL,
			rotating_cb=self.__channel_rotating_cb,
			timer_id=None
		)

		self.__led = Pin(2, Pin.OUT, Pin.PULL_DOWN)

		self.__start_codec()
		# self.__start_mic()

#region start & stop mic
	def __start_mic(self):
		if self.__mic is None:
			self.__mic = I2S(
				0,
				sck=Pins.I2S_MIC.SCK, ws=Pins.I2S_MIC.WS, sd=Pins.I2S_MIC.SD,
				mode=I2S.RX,
				bits=BITS, format=FORMAT, rate=RATE,
				ibuf=BUFF_SIZE
			)

			sleep_ms(100)

	def __stop_mic(self):
		if self.__mic is not None:
			self.__mic.deinit()
			self.__mic = None

			sleep_ms(200)
#endregion

#region start & stop codec
	def __start_codec(self):
		if self.__codec is None:
			self.__codec = I2S(
				1,
				sck=Pins.I2S_CODEC.SCK, ws=Pins.I2S_CODEC.WS, sd=Pins.I2S_CODEC.SD,
				mode=I2S.TX,
				bits=BITS, format=FORMAT, rate=RATE,
				ibuf=BUFF_SIZE
			)

			sleep_ms(200)

	def __stop_codec(self):
		if self.__codec is not None:
			self.__codec.deinit()
			self.__codec = None

			sleep_ms(200)
#endregion

#region button & touchpad & channel_adc callback functions
	def __button_hold_cb(self, pin):
		self.__led.on()
		# printf(f'button {pin} holding')

	def __button_release_cb(self, pin):
		self.__led.off()
		# printf(f'button {pin} released')

	def __button_click_cb(self, pin):
		# printf(f'button {pin} clicked')

		channel_offset = 0

		if pin == Pins.KEYS.CHANNEL_UP:
			channel_offset = 1
		elif pin == Pins.KEYS.CHANNEL_DOWN:
			channel_offset = -1

		self.__channel = (self.__channel + channel_offset) % self.__max_channel
		printf(f'button channel {self.__channel}')

		self.__clean_rect(0, 0, self.__fontlib.font_width * 4, self.__fontlib.font_height)
		self.__show_font(f'CH{self.__channel + 1}')

		self.__play_sound_effect_udp(SOUND_EFFECT_CLICK3)

	def __touchpad_cb(self, pin, touched):
		self.__led.value(touched)
		printf(f'touchpad {pin} {"holding" if touched else "released"}')

		if touched:
			# self.__stop_codec()
			self.__start_mic()

			self.__dispatcher.add_work(self.__task_send_wave_sample_via_udp, 20)
		else:
			self.__dispatcher.del_work(self.__task_send_wave_sample_via_udp)

			self.__stop_mic()
			# self.__start_codec()

			self.__play_sound_effect_udp(SOUND_EFFECT_SIGNALING1)

	def __channel_rotating_cb(self, pin, direction, sampling):
		channel_now = int(sampling / self.__scale_range)

		if self.__channel != channel_now and channel_now < self.__max_channel:
			printf(f'channel: {channel_now}, {sampling}')
			self.__channel = channel_now

			self.__clean_rect(0, 0, self.__fontlib.font_width * 4, self.__fontlib.font_height)
			self.__show_font(f'CH{self.__channel + 1}')
			self.__play_sound_effect(SOUND_EFFECT_CLICK2)
#endregion

	def __send_wave_sample_via_udp(self):
		length = self.__mic.readinto(self.__udp_client.wave_sample_mv)
		I2S.shift(buf=self.__udp_client.wave_sample_mv, bits=16, shift=3)

		self.__udp_client.send(length)

	def __play_sound_effect_udp(self, sound_file):
		with open(sound_file, 'rb') as sound_effect:
			sound_effect.seek(44)

			buffer = memoryview(bytearray(1000))
			length = sound_effect.readinto(buffer)

			while length:
				self.__udp_client.send(length, buffer)
				length = sound_effect.readinto(buffer)

	def __play_sound_effect(self, sound_file):
		with open(sound_file, 'rb') as sound_effect:
			sound_effect.seek(44)

			buffer = memoryview(bytearray(1000))
			length = sound_effect.readinto(buffer)

			while length:
				self.__codec.write(buffer[0:length])
				length = sound_effect.readinto(buffer)

	def __clean_display(self):
		if self.__display is None or self.__fontlib is None:
			return
		
		self.__display.fill(0)
		# self.__display.show()

	def __clean_rect(self, x, y, width, height):
		if self.__display is None or self.__fontlib is None:
			return

		self.__display.fill_rect(x, y, width, height, 0)
		# self.__display.show()

	def __show_font(self, chars, x=0, y=0, center=False):
		if self.__display is None or self.__fontlib is None:
			return

		if center:
			x = int((128 - self.__fontlib.font_width * len(chars)) / 2)
			y = int((64 - self.__fontlib.font_height) / 2)

		def __fill_buffer(buffer, width, height, x, y):
			fb = framebuf.FrameBuffer(bytearray(buffer), width, height, self.__fontlib.format)
			self.__display.blit(fb, x, y)

		width  = self.__fontlib.font_width
		height = self.__fontlib.font_height
		buffer_list = self.__fontlib.get_characters(chars)

		for char in chars:
			buffer = memoryview(buffer_list[ord(char)])

			if x > ((128 // width - 1) * width):
				x = 0
				y += height

			__fill_buffer(buffer, width, height, x, y)
			x += width

		self.__display.show()


if __name__ == '__main__':
	board = VerifyBoard()
